package com.cloudbroker.bcs.common.http;

import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cloudbroker.bcs.common.util.IOUtil;
import com.cloudbroker.bcs.common.util.StringUtil;
import com.cloudbroker.bcs.common.util.ValueUtil;

public class HttpClient {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClient.class);
    
    private static RequestPayloadHandler<Object> stringParamRequestHandler = new RequestPayloadHandler<Object>() {
        @Override
        public void handle(HttpEntityEnclosingRequest request, Object param, String mimeType, String charset) {
            if (!ValueUtil.isEmpty(param)) {
                if (null == mimeType) {
                    mimeType = "text/plain";
                }
                String string = String.valueOf(param);
                HttpEntity postBody = new StringEntity(string, ContentType.create(mimeType, charset));
                request.setEntity(postBody);
            }
        }
    };
    private static RequestPayloadHandler<Object> jsonParamRequestHandler = new RequestPayloadHandler<Object>() {
        @Override
        public void handle(HttpEntityEnclosingRequest request, Object param, String mimeType, String charset) {
            if (!ValueUtil.isEmpty(param)) {
                String jsonString = JSON.toJSONString(param);
                HttpEntity postBody = new StringEntity(jsonString, ContentType.APPLICATION_JSON);
                request.setEntity(postBody);
            }
        }
    };
    private static RequestPayloadHandler<Map<String, ?>> formParamRequestHandler = new RequestPayloadHandler<Map<String, ?>>() {
        @Override
        public void handle(HttpEntityEnclosingRequest request, Map<String, ?> param, String mimeType, String charset) {
            if (!ValueUtil.isEmpty(param)) {
                HttpEntity postBody = mapToFormEntity(param, charset);
                request.setEntity(postBody);
            }
        }
    };
    
    protected int connectTimeout = 30000;
    
    protected int socketTimeout = 30000;
    
    protected String pointLocal = "local";
    
    protected String pointRemote = "remote";
    
    public int getConnectTimeout() {
        return connectTimeout;
    }
    
    public HttpClient setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
        return this;
    }
    
    public int getSocketTimeout() {
        return socketTimeout;
    }
    
    public HttpClient setSocketTimeout(int socketTimeout) {
        this.socketTimeout = socketTimeout;
        return this;
    }
    
    public String getPointLocal() {
        return pointLocal;
    }
    
    public void setPointLocal(String pointLocal) {
        this.pointLocal = pointLocal;
    }
    
    public String getPointRemote() {
        return pointRemote;
    }
    
    public void setPointRemote(String pointRemote) {
        this.pointRemote = pointRemote;
    }
    
    protected RequestConfig getDefaultRequestConfig() {
        return RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
    }
    
    protected HttpClientBuilder getDefaultClientBuilder() {
        return HttpClients.custom();
    }
    
    protected CloseableHttpClient getHttpClient(String url) throws Exception {
        if ("https".equals(new URL(url).getProtocol())) {
            return getHttpsClient();
        } else {
            return getHttpClient();
        }
    }
    
    protected CloseableHttpClient getHttpClient() {
        return getDefaultClientBuilder().build();
    }
    
    public static CloseableHttpClient getHttpsClient() throws Exception {
        try {
            TrustManager[] trustManagers = new TrustManager[] { new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString)
                        throws CertificateException {
                }
                
                public void checkServerTrusted(X509Certificate[] paramArrayOfX509Certificate, String paramString)
                        throws CertificateException {
                }
                
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            } };
            SSLContext sslContext = SSLContext.getInstance(SSLConnectionSocketFactory.TLS);
            sslContext.init(new KeyManager[0], trustManagers, new SecureRandom());
            SSLContext.setDefault(sslContext);
            sslContext.init(null, trustManagers, null);
            SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
            HttpClientBuilder clientBuilder = HttpClients.custom().setSSLSocketFactory(connectionSocketFactory);
            clientBuilder.setRedirectStrategy(new LaxRedirectStrategy());
            CloseableHttpClient httpClient = clientBuilder.build();
            return httpClient;
        } catch (Exception e) {
            throw new Exception("http client 远程连接失败", e);
        }
    }
    
    protected String genRequestLogHead(String itfDesc) {
        StringBuilder sBuilder = new StringBuilder(32).append('[').append(pointLocal).append('-').append('>')
                .append(pointRemote).append(']');
        if (null != itfDesc) {
            sBuilder.append('[').append(itfDesc).append(']');
        }
        return sBuilder.append(' ').toString();
    }
    
    protected String genResponseLogHead(String itfDesc) {
        StringBuilder sBuilder = new StringBuilder(32).append('[').append(pointLocal).append('<').append('-')
                .append(pointRemote).append(']');
        if (null != itfDesc) {
            sBuilder.append('[').append(itfDesc).append(']');
        }
        return sBuilder.append(' ').toString();
    }
    
    protected <T> String postFetchString(RequestPayloadHandler<T> requestPayloadHandler, String url, T param,
            String itfDesc, String mimeType, String charset) throws ConnectTimeoutException, SocketTimeoutException {
        CloseableHttpClient client = null;
        CloseableHttpResponse response = null;
        try {
            LOGGER.info(genRequestLogHead(itfDesc) + param);
            
            client = getHttpClient(url);
            
            HttpPost request = new HttpPost(url);
            request.setConfig(getDefaultRequestConfig());
            
            requestPayloadHandler.handle(request, param, mimeType, charset);
            
            response = client.execute(request);
            
            HttpEntity responseEntity = response.getEntity();
            if (null != responseEntity) {
                String responseString = EntityUtils.toString(responseEntity);
                LOGGER.info(genResponseLogHead(itfDesc) + responseString);
                
                return responseString;
            } else {
                LOGGER.error(genResponseLogHead(itfDesc) + "doExecute failed, responseEntity is null");
            }
        } catch (ConnectTimeoutException e) {
            LOGGER.error(genResponseLogHead(itfDesc) + "doExecute failed", e);
            throw e;
        } catch (SocketTimeoutException e) {
            LOGGER.error(genResponseLogHead(itfDesc) + "doExecute failed", e);
            throw e;
        } catch (Exception e) {
            LOGGER.error(genResponseLogHead(itfDesc) + "doExecute failed", e);
        } finally {
            IOUtil.close(response);
            IOUtil.close(client);
        }
        return null;
    }
    
    public String postJSONFetchString(String url, Object param, String itfDesc) throws ConnectTimeoutException,
            SocketTimeoutException {
        return postFetchString(jsonParamRequestHandler, url, param, itfDesc, null, null);
    }
    
    public JSONObject postJSONFetchJSON(String url, Object param, String itfDesc) throws ConnectTimeoutException,
            SocketTimeoutException {
        String responseString = postJSONFetchString(url, param, itfDesc);
        if (StringUtils.isNotBlank(responseString)) {
            return JSON.parseObject(responseString);
        }
        return null;
    }
    
    public JSONObject postJSONFetchJSON(String url, Object param) throws ConnectTimeoutException,
            SocketTimeoutException {
        return postJSONFetchJSON(url, param, null);
    }
    
    protected static UrlEncodedFormEntity mapToFormEntity(Map<String, ?> param, String charset) {
        List<NameValuePair> nvPairs = new LinkedList<NameValuePair>();
        for (Entry<String, ?> entry : param.entrySet()) {
            String keyStr = StringUtil.nullToEmpty(entry.getKey());
            Object value = entry.getValue();
            String valueStr = StringUtil.nullToEmpty(value);
            nvPairs.add(new BasicNameValuePair(keyStr, valueStr));
        }
        try {
            return new UrlEncodedFormEntity(nvPairs, charset);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unsupported encoding: " + charset, e);
        }
    }
    
    public String postFormFetchString(String url, Map<String, ?> param, String itfDesc, String charset)
            throws ConnectTimeoutException, SocketTimeoutException {
        return postFetchString(formParamRequestHandler, url, param, itfDesc, null, charset);
    }
    
    public Map<String, String> postFormFetchForm(String url, Map<String, ?> param, String itfDesc, String charset)
            throws ConnectTimeoutException, SocketTimeoutException {
        String responseString = postFormFetchString(url, param, itfDesc, charset);
        if (StringUtils.isNotBlank(responseString)) {
            return parseUrlEncodedString(responseString, charset);
        }
        return null;
    }
    
    public static Map<String, String> parseUrlEncodedString(String string, String charset) {
        if (StringUtils.isBlank(string)) {
            return new HashMap<String, String>();
        }
        try {
            string = URLDecoder.decode(string, charset);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unsupported encoding " + charset, e);
        }
        String[] nvPairs = string.split("&");
        Map<String, String> map = new LinkedHashMap<String, String>(nvPairs.length);
        for (String nvPairString : nvPairs) {
            String[] nvPair = nvPairString.split("=", -1);
            if (nvPair.length == 2) {
                map.put(nvPair[0], nvPair[1]);
            }
        }
        return map;
    }
    
    public JSONObject postFormFetchJSON(String url, Map<String, ?> param, String itfDesc, String charset)
            throws ConnectTimeoutException, SocketTimeoutException {
        String responseString = postFormFetchString(url, param, itfDesc, charset);
        if (StringUtils.isNotBlank(responseString)) {
            return JSON.parseObject(responseString);
        }
        return null;
    }
    
    public String postStringFetchString(String url, String param, String itfDesc, String mimeType, String charset)
            throws ConnectTimeoutException, SocketTimeoutException {
        return postFetchString(stringParamRequestHandler, url, param, itfDesc, mimeType, charset);
    }
    
    public String postStringFetchString(String url, String param, String itfDesc, String charset)
            throws ConnectTimeoutException, SocketTimeoutException {
        return postStringFetchString(url, param, itfDesc, null, charset);
    }
    
    protected static interface RequestPayloadHandler<T> {
        
        public void handle(HttpEntityEnclosingRequest request, T param, String mimeType, String charset);
        
    }
    
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("HttpClient [socketTimeout=").append(socketTimeout).append(", connectTimeout=")
                .append(connectTimeout).append(", pointLocal=").append(pointLocal).append(", pointRemote=")
                .append(pointRemote).append("]");
        return builder.toString();
    }
    
}
